using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.DirectoryServices.Protocols;
using System.Net;
using System.Threading;
using InfiniTec.Threading;

namespace InfiniTec.DirectoryServices
{
	/// <summary>
	/// The Connection class provides access to an ActiveDirectory instance by using
	/// an <see cref="LdapConnection"/> instance as well as a native connection.
	/// </summary>
	public class Connection : IDisposable
	{
		private readonly ManualResetEvent _InitializationWaitHandle;
		private readonly object _NativeConnectionLock = new object();
		private LdapConnection _Connection;
		private DirectoryServiceConnectionSafeHandle _DirectoryConnection;
		private bool _Initialized;
		private int _Initializing;

		/// <summary>
		/// Initializes a new instance of the <see cref="Connection"/> class.
		/// </summary>
		/// <param name="domainBinding">The domain binding.</param>
		/// <param name="type">The type of the specified domain binding.</param>
		/// <param name="connectionLess">if set to <see langword="true" /> [connection less].</param>
		public Connection(string domainBinding, DirectoryIdentifierType type, bool connectionLess)
		{
			_InitializationWaitHandle = new ManualResetEvent(false);

			if (type == DirectoryIdentifierType.DnsDomain)
			{
				DnsDomainName = domainBinding;
			}
			else if (type == DirectoryIdentifierType.Server)
			{
				DomainController = domainBinding;
			}
			else
			{
				throw new ArgumentException("The specified directory identifier type is invalid.", "type");
			}
			_Connection =
				new LdapConnection(new LdapDirectoryIdentifier(domainBinding,
				                                               (type == DirectoryIdentifierType.DnsDomain) ? true : false,
				                                               connectionLess));
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="Connection"/> class.
		/// </summary>
		public Connection() : this(null, DirectoryIdentifierType.DnsDomain, false)
		{
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="Connection"/> class.
		/// </summary>
		/// <param name="domainBinding">The domain binding to use.</param>
		/// <param name="type">The type of the domain binding (either server or DNS domain name).</param>
		/// <param name="connectionLess">if set to <see langword="true" />, the connection is connection-less; otherwise, the connection is kept alive.</param>
		/// <param name="credential">The credential to use.</param>
		/// <param name="authType">The type of the authentication to use.</param>
		public Connection(string domainBinding, DirectoryIdentifierType type, bool connectionLess,
		                  NetworkCredential credential, AuthType authType)
			: this(domainBinding, type, connectionLess)
		{
			Credential = credential;
			AuthenticationType = authType;
		}

		/// <summary>
		/// Gets or sets the domain controller.
		/// </summary>
		/// <value>The domain controller.</value>
		public string DomainController { [DebuggerStepThrough]
		get; [DebuggerStepThrough]
		set; }

		internal object NativeConnectionLock
		{
			get { return _NativeConnectionLock; }
		}

		internal DirectoryServiceConnectionSafeHandle DirectoryConnection
		{
			[DebuggerStepThrough]
			get
			{
				EnsureNativeDirectoryConnection();
				return _DirectoryConnection;
			}
		}

		/// <summary>
		/// Gets the session options.
		/// </summary>
		/// <value>The session options.</value>
		public LdapSessionOptions SessionOptions
		{
			[DebuggerStepThrough]
			get { return _Connection.SessionOptions; }
		}

		/// <summary>
		/// Specifies, whether partial results are returned during a query.
		/// </summary>
		public PartialResultProcessing PartialResultProcessing { [DebuggerStepThrough]
		get; [DebuggerStepThrough]
		set; }


		/// <summary>
		/// Gets or sets the <see cref="AuthType"/> which is used to connect to the directory server.
		/// </summary>
		public AuthType AuthenticationType { [DebuggerStepThrough]
		get; [DebuggerStepThrough]
		set; }

		/// <summary>
		/// Gets or sets the credentials which are used to connect to the directory server.
		/// If set to null, integrated authentication is used.
		/// </summary>
		public NetworkCredential Credential { [DebuggerStepThrough]
		get; [DebuggerStepThrough]
		set; }

		/// <summary>
		/// Specifies the host to bind to when performing Active Directory requests.
		/// This can bei either a computername, IP address or full qualified domain name.
		/// If set to null, a serverless binding is performed.
		/// </summary>
		public string DnsDomainName { [DebuggerStepThrough]
		get; [DebuggerStepThrough]
		set; }

		/// <summary>
		/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
		/// </summary>
		public void Dispose()
		{
			Dispose(true);
		}

		private void EnsureNativeDirectoryConnection()
		{
			lock (_NativeConnectionLock)
			{
				if (_DirectoryConnection != null) return;

				//if (!_Initialized) Initialize();

				_DirectoryConnection = DirectoryServiceConnectionSafeHandle.Create(
					DomainController, DnsDomainName, Credential);
			}
		}

		/// <summary>
		/// Gets an asynchronous operation which can be used to perform a request.
		/// </summary>
		/// <param name="request">The request to perform.</param>
		/// <param name="userState">A user-defined object that is passed to the method invoked when the asynchronous operation completes.</param>
		/// <returns>An async operation.</returns>
		public AsyncOperation<DirectoryResponse> GetSendRequestOperation(DirectoryRequest request, object userState)
		{
			return new AsyncOperation<DirectoryResponse>(SendRequestInternal<object>(request, false, null), userState);
		}

		/// <summary>
		/// Gets an asynchronous operation which can be used to perform a request.
		/// </summary>
		/// <typeparam name="TPartialResult">The type of the partial result.</typeparam>
		/// <param name="request">The request to perform.</param>
		/// <param name="partialResultConverter">A callback which is called to transform partial results before passing them to the progress changed event handler.</param>
		/// <param name="userState">A user-defined object that is passed to the method invoked when the asynchronous operation completes.</param>
		/// <returns>An async operation.</returns>
		public AsyncOperation<DirectoryResponse> GetSendRequestOperation<TPartialResult>(DirectoryRequest request,
		                                                                                 Converter<object, TPartialResult>
		                                                                                 	partialResultConverter,
		                                                                                 object userState)
			where TPartialResult : class
		{
			return new AsyncOperation<DirectoryResponse>(SendRequestInternal(request, true, partialResultConverter), userState);
		}

		private IEnumerable<OperationAction> SendRequestInternal<TPartialResult>(DirectoryRequest request,
		                                                                         bool enablePartialResults,
		                                                                         Converter<object, TPartialResult>
		                                                                         	partialResultConverter)
			where TPartialResult : class
		{
			IAsyncResult ar = null;
			LdapConnection connection = null;

			try
			{
				if (!_Initialized)
				{
					if (Interlocked.Exchange(ref _Initializing, 1) == 1)
					{
						_InitializationWaitHandle.WaitOne();
						if (!_Initialized) throw new InitializationFailedException("The initialization failed during another call");
					}
					else
					{
						Initialize();
					}
				}
				connection = _Connection;
				ar = connection.BeginSendRequest(request,
				                                 enablePartialResults
				                                 	? PartialResultProcessing.ReturnPartialResults
				                                 	: PartialResultProcessing.NoPartialResultSupport, null, null);

				yield return new WaitForHandleOperation(ar.AsyncWaitHandle);

				if (enablePartialResults)
				{
					while (!ar.IsCompleted)
					{
						PartialResultsCollection partialResult = _Connection.GetPartialResults(ar);

						IList<TPartialResult> partialResultItems = ConvertItems(partialResult, partialResultConverter);

						yield return new WaitForHandleOperation(ar.AsyncWaitHandle, partialResultItems);
					}
				}

				DirectoryResponse response;
				try
				{
					response = connection.EndSendRequest(ar);
				}
				catch
				{
					ar = null;
					throw;
				}

				ar = null;

				yield return new OperationResult<DirectoryResponse>(response);
			}
			finally
			{
				if (ar != null)
				{
					connection.Abort(ar);
				}
			}
		}

		private static IList<TPartialResult> ConvertItems<TPartialResult>(PartialResultsCollection result,
		                                                                  Converter<object, TPartialResult>
		                                                                  	partialResultConverter)
			where TPartialResult : class
		{
			var partialResultItems = new List<TPartialResult>();

			foreach (object entry in result)
			{
				TPartialResult resultItem;
				if (partialResultConverter == null)
				{
					resultItem = entry as TPartialResult;
					if (resultItem == null)
						throw new InvalidOperationException("The partial result item cannot be cast to type " +
						                                    typeof (TPartialResult).Name + " and no converter was provided");

					partialResultItems.Add(resultItem);
				}
				else
				{
					resultItem = partialResultConverter(entry);
					if (resultItem == null) continue;

					partialResultItems.Add(resultItem);
				}
			}
			return partialResultItems.AsReadOnly();
		}

		private void Initialize()
		{
			_Connection.Bind(Credential);
			_Initialized = true;
			_Initializing = 0;

			_InitializationWaitHandle.Set();
		}

		/// <summary>
		/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
		/// </summary>
		/// <param name="disposing">if set to <see langword="true" />, the method is called from the <see cref="Dispose()"/> method. Otherwise, it is called
		/// from a destructor.</param>
		protected virtual void Dispose(bool disposing)
		{
			DirectoryServiceConnectionSafeHandle internalConnection;

			if (!disposing) return;

			LdapConnection connection = Interlocked.Exchange(ref _Connection, null);

			lock (_NativeConnectionLock)
			{
				internalConnection = Interlocked.Exchange(ref _DirectoryConnection, null);
			}

			if (internalConnection != null) internalConnection.Dispose();
			if (connection != null) connection.Dispose();
		}
	}
}